﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace FriendAnalyzer.Infrastructure.MachineLearning.Analysis
{
    public class FuzzyCMeans<T>
    {
        private uint k;
        private T[] database { get; set; }
        private T[] centroids = null;
        private double[][] membership;
        private double error;

        public T[] Database
        {
            get { return database; }
        }

        public uint K
        {
            get { return k; }
        }

        public double Accuracy { get; set; }

        public double Fuzzyness { get; set; }

        public double MaxStepCount { get; set; }

        protected Func<T, T, double> DistanceFunc { get; set; }

        protected Func<T[], double[], double, T> CentroidFunc;

        public T[] Centroids
        {
            get { return centroids ?? new T[0]; }
            set { this.centroids = value; }
        }

        public double Error
        {
            get { return error; }
        }

        public FuzzyCMeans(T[] database, uint k)
        {
            this.database = database;
            this.k = k;

            this.membership = new double[Database.Length][];
            for (int i = 0; i < Database.Length; i++)
                this.membership[i] = new double[K];
            for (int i = 0; i < Database.Length; i++)
                for (int j = 0; j < K; j++)
                    membership[i][j] = 0;
        }

        private void AssignRandomCentroids()
        {
            Random random = new Random();
            List<int> selectedIndices = new List<int>();
            centroids = new T[K];
            for (int i = 0; i < K; i++)
            {
                int index;
                while (selectedIndices.Contains(index = random.Next(0, Database.Length - 1)));
                centroids[i] = Database[index];
                selectedIndices.Add(index);
            }
            //centroids = Enumerable.Range(0, k).Select(x => database[x]).ToArray();
        }

        public void Compute(bool recomputeCentroids)
        {
            if (this.K > this.Database.Length) return;
            //assign random examples form the database to centroids, if no previews assignment
            if (this.centroids == null)
                this.AssignRandomCentroids();

            double previousObjValue, currentObjValue;
            int stepCount = 0;
            do
            {
                //recompute the membership matrix
                for (int i = 0; i < Database.Length; i++)
                {
                    double distSum = .0;
                    for (int j = 0; j < K; j++)
                        distSum += Math.Pow(1.0 / DistanceFunc(Database[i], centroids[j]), 2.0 / (Fuzzyness - 1));

                    for (int j = 0; j < K; j++)
                        membership[i][j] = Math.Pow(1.0 / DistanceFunc(Database[i], centroids[j]), 2.0 / (Fuzzyness - 1)) / distSum;
                }

                previousObjValue = GetObjectiveFunctionValue();

                if (recomputeCentroids)
                {
                    //recompute the centroids
                    for (int j = 0; j < K; j++)
                        centroids[j] = CentroidFunc(Database, Enumerable.Range(0, Database.Length).Select(i => membership[i][j]).ToArray(), Fuzzyness);
                }

                currentObjValue = GetObjectiveFunctionValue();
                stepCount++;
            }
            while (Math.Abs(currentObjValue - previousObjValue) >= Accuracy && stepCount < MaxStepCount);

            error = currentObjValue;
        }

        private double GetObjectiveFunctionValue()
        {
            double objValue = 0;
            for (int i = 0; i < Database.Length; i++)
                for (int j = 0; j < K; j++)
                    objValue += Math.Pow(membership[i][j], Fuzzyness) * Math.Pow(DistanceFunc(Database[i], centroids[j]), 2);

            return objValue;
        }

        public T[] GetClusterMembers(int j)
        {
            if (j >= K || K > Database.Length) return new T[0];
            return Enumerable.Range(0, Database.Length).Where(i => membership[i][j] == membership[i].Max()).Select(i => Database[i]).ToArray();
        }
    }
}
